# frozen_string_literal: true

require "spec_helper"
require "split/trial"

describe Split::Trial do
  let(:user) { mock_user }
  let(:alternatives) { ["basket", "cart"] }
  let(:experiment) do
    Split::Experiment.new("basket_text", alternatives: alternatives).save
  end

  it "should be initializeable" do
    experiment  = double("experiment")
    alternative = double("alternative", kind_of?: Split::Alternative)
    trial = Split::Trial.new(experiment: experiment, alternative: alternative)
    expect(trial.experiment).to eq(experiment)
    expect(trial.alternative).to eq(alternative)
  end

  describe "alternative" do
    it "should use the alternative if specified" do
      alternative = double("alternative", kind_of?: Split::Alternative)
      trial = Split::Trial.new(experiment: double("experiment"),
          alternative: alternative, user: user)
      expect(trial).not_to receive(:choose)
      expect(trial.alternative).to eq(alternative)
    end

    it "should load the alternative when the alternative name is set" do
      experiment = Split::Experiment.new("basket_text", alternatives: ["basket", "cart"])
      experiment.save

      trial = Split::Trial.new(experiment: experiment, alternative: "basket")
      expect(trial.alternative.name).to eq("basket")
    end
  end

  describe "metadata" do
    let(:metadata) { Hash[alternatives.map { |k| [k, "Metadata for #{k}"] }] }
    let(:experiment) do
      Split::Experiment.new("basket_text", alternatives: alternatives, metadata: metadata).save
    end

    it "has metadata on each trial" do
      trial = Split::Trial.new(experiment: experiment, user: user, metadata: metadata["cart"],
                               override: "cart")
      expect(trial.metadata).to eq(metadata["cart"])
    end

    it "has metadata on each trial from the experiment" do
      trial = Split::Trial.new(experiment: experiment, user: user)
      trial.choose!
      expect(trial.metadata).to eq(metadata[trial.alternative.name])
      expect(trial.metadata).to match(/#{trial.alternative.name}/)
    end
  end

  describe "#choose!" do
    let(:context) { double(on_trial_callback: "test callback") }
    let(:trial) do
      Split::Trial.new(user: user, experiment: experiment)
    end

    shared_examples_for "a trial with callbacks" do
      it "does not run if on_trial callback is not respondable" do
        Split.configuration.on_trial = :foo
        allow(context).to receive(:respond_to?).with(:foo, true).and_return false
        expect(context).to_not receive(:foo)
        trial.choose! context
      end
      it "runs on_trial callback" do
        Split.configuration.on_trial = :on_trial_callback
        expect(context).to receive(:on_trial_callback)
        trial.choose! context
      end
      it "does not run nil on_trial callback" do
        Split.configuration.on_trial = nil
        expect(context).not_to receive(:on_trial_callback)
        trial.choose! context
      end
    end

    def expect_alternative(trial, alternative_name)
      3.times do
        trial.choose! context
        expect(alternative_name).to include(trial.alternative.name)
      end
    end

    context "when override is present" do
      let(:override) { "cart" }
      let(:trial) do
        Split::Trial.new(user: user, experiment: experiment, override: override)
      end

      it_behaves_like "a trial with callbacks"

      it "picks the override" do
        expect(experiment).to_not receive(:next_alternative)
        expect_alternative(trial, override)
      end

      context "when alternative doesn't exist" do
        let(:override) { nil }
        it "falls back on next_alternative" do
          expect(experiment).to receive(:next_alternative).and_call_original
          expect_alternative(trial, alternatives)
        end
      end
    end

    context "when disabled option is true" do
      let(:trial) do
        Split::Trial.new(user: user, experiment: experiment, disabled: true)
      end

      it "picks the control", :aggregate_failures do
        Split.configuration.on_trial = :on_trial_callback
        expect(experiment).to_not receive(:next_alternative)

        expect(context).not_to receive(:on_trial_callback)

        expect_alternative(trial, "basket")
        Split.configuration.on_trial = nil
      end
    end

    context "when Split is globally disabled" do
      it "picks the control and does not run on_trial callbacks", :aggregate_failures do
        Split.configuration.enabled = false
        Split.configuration.on_trial = :on_trial_callback

        expect(experiment).to_not receive(:next_alternative)
        expect(context).not_to receive(:on_trial_callback)
        expect_alternative(trial, "basket")

        Split.configuration.enabled = true
        Split.configuration.on_trial = nil
      end
    end

    context "when experiment has winner" do
      let(:trial) do
        Split::Trial.new(user: user, experiment: experiment)
      end

      it_behaves_like "a trial with callbacks"

      it "picks the winner" do
        experiment.winner = "cart"
        expect(experiment).to_not receive(:next_alternative)

        expect_alternative(trial, "cart")
      end
    end

    context "when exclude is true" do
      let(:trial) do
        Split::Trial.new(user: user, experiment: experiment, exclude: true)
      end

      it_behaves_like "a trial with callbacks"

      it "picks the control" do
        expect(experiment).to_not receive(:next_alternative)
        expect_alternative(trial, "basket")
      end
    end

    context "when user is already participating" do
      it_behaves_like "a trial with callbacks"

      it "picks the same alternative" do
        user[experiment.key] = "basket"
        expect(experiment).to_not receive(:next_alternative)

        expect_alternative(trial, "basket")
      end

      context "when alternative is not found" do
        it "falls back on next_alternative" do
          user[experiment.key] = "notfound"
          expect(experiment).to receive(:next_alternative).and_call_original
          expect_alternative(trial, alternatives)
        end
      end
    end

    context "when user is a new participant" do
      it "picks a new alternative and runs on_trial_choose callback", :aggregate_failures do
        Split.configuration.on_trial_choose = :on_trial_choose_callback

        expect(experiment).to receive(:next_alternative).and_call_original
        expect(context).to receive(:on_trial_choose_callback)

        trial.choose! context

        expect(trial.alternative.name).to_not be_empty
        Split.configuration.on_trial_choose = nil
      end

      it "assigns user to an alternative" do
        trial.choose! context

        expect(alternatives).to include(user[experiment.name])
      end

      context "when cohorting is disabled" do
        before(:each) { allow(experiment).to receive(:cohorting_disabled?).and_return(true) }

        it "picks the control and does not run on_trial callbacks" do
          Split.configuration.on_trial = :on_trial_callback

          expect(experiment).to_not receive(:next_alternative)
          expect(context).not_to receive(:on_trial_callback)
          expect_alternative(trial, "basket")

          Split.configuration.enabled = true
          Split.configuration.on_trial = nil
        end

        it "user is not assigned an alternative" do
          trial.choose! context

          expect(user[experiment]).to eq(nil)
        end
      end
    end
  end

  describe "#complete!" do
    context "when there are no goals" do
      let(:trial) { Split::Trial.new(user: user, experiment: experiment) }
      it "should complete the trial" do
        trial.choose!
        old_completed_count = trial.alternative.completed_count
        trial.complete!
        expect(trial.alternative.completed_count).to eq(old_completed_count + 1)
      end
    end

    context "when there are many goals" do
      let(:goals) { [ "goal1", "goal2" ] }
      let(:trial) { Split::Trial.new(user: user, experiment: experiment, goals: goals) }

      it "increments the completed count corresponding to the goals" do
        trial.choose!
        old_completed_counts = goals.map { |goal| [goal, trial.alternative.completed_count(goal)] }.to_h
        trial.complete!
        goals.each { | goal | expect(trial.alternative.completed_count(goal)).to eq(old_completed_counts[goal] + 1) }
      end
    end

    context "when there is 1 goal of type string" do
      let(:goal) { "goal" }
      let(:trial) { Split::Trial.new(user: user, experiment: experiment, goals: goal) }
      it "increments the completed count corresponding to the goal" do
        trial.choose!
        old_completed_count = trial.alternative.completed_count(goal)
        trial.complete!
        expect(trial.alternative.completed_count(goal)).to eq(old_completed_count + 1)
      end
    end
  end

  describe "alternative recording" do
    before(:each) { Split.configuration.store_override = false }

    context "when override is present" do
      it "stores when store_override is true" do
        trial = Split::Trial.new(user: user, experiment: experiment, override: "basket")

        Split.configuration.store_override = true
        expect(user).to receive("[]=")
        trial.choose!
        expect(trial.alternative.participant_count).to eq(1)
      end

      it "does not store when store_override is false" do
        trial = Split::Trial.new(user: user, experiment: experiment, override: "basket")

        expect(user).to_not receive("[]=")
        trial.choose!
      end
    end

    context "when disabled is present" do
      it "stores when store_override is true" do
        trial = Split::Trial.new(user: user, experiment: experiment, disabled: true)

        Split.configuration.store_override = true
        expect(user).to receive("[]=")
        trial.choose!
      end

      it "does not store when store_override is false" do
        trial = Split::Trial.new(user: user, experiment: experiment, disabled: true)

        expect(user).to_not receive("[]=")
        trial.choose!
      end
    end

    context "when exclude is present" do
      it "does not store" do
        trial = Split::Trial.new(user: user, experiment: experiment, exclude: true)

        expect(user).to_not receive("[]=")
        trial.choose!
      end
    end

    context "when experiment has winner" do
      let(:trial) do
        experiment.winner = "cart"
        Split::Trial.new(user: user, experiment: experiment)
      end

      it "does not store" do
        expect(user).to_not receive("[]=")
        trial.choose!
      end
    end
  end
end
